Skip to content

Conversation

@sidneyswift
Copy link
Contributor

@sidneyswift sidneyswift commented Jan 19, 2026

Summary

Adds a new API endpoint for creating workspaces with proper authentication and organization linking.

Changes

  • POST /api/workspaces - Create workspace with Bearer token or API key auth
  • Supports organization_id parameter to link workspace to an org
  • Validates organization access before linking
  • Creates account, account_info, and owner link in one transaction

Files Added

  • app/api/workspaces/route.ts - API route
  • lib/workspaces/createWorkspaceInDb.ts - Database creation logic
  • lib/workspaces/createWorkspacePostHandler.ts - Request handler
  • lib/workspaces/validateCreateWorkspaceBody.ts - Auth + validation
  • lib/supabase/account_workspace_ids/insertAccountWorkspaceId.ts - Owner link helper

Note

Modernizes auth and expands APIs and tooling.

  • Centralized auth: Adds validateAuthContext (API key or Bearer) with org/account access checks; refactors POST /api/artists validation/handler and tests to use it
  • New endpoint: Adds POST /api/workspaces with CORS OPTIONS, validation, and owner linkage via insertAccountWorkspaceId
  • Composio integration: Adds tool router session and tools (composio, connectors list/authorize/disconnect helpers, ownership validation) and Google Sheets flow support
  • Chat pipeline: Improves streaming/completion handling, tool-chain step preparation, and utilities; broad unit/integration tests across chat modules
  • AI model utilities: Adds getAvailableModels, getModel, isEmbedModel with tests; credit usage handling improvements
  • Misc utilities: Small API/comment cleanups, organizations/body validators, files/messages/email helpers; extensive test coverage added

Written by Cursor Bugbot for commit 78229b5. This will update automatically on new commits. Configure here.

- Create insertAccountWorkspaceId helper for owner-workspace linking
- Create createWorkspaceInDb with account, info, owner link, and org link
- Add validateCreateWorkspaceBody with API key and Bearer token auth
- Add organization access validation for workspace linking
- Expose POST /api/workspaces route with CORS support
@vercel
Copy link

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
recoup-api Ready Ready Preview Jan 20, 2026 7:31pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +43 to +45
if (organizationId) {
await addArtistToOrganization(account.id, organizationId);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organization linking failures are silently ignored, causing the API to return success (201) even when the workspace fails to link to the requested organization.

View Details
📝 Patch Details
diff --git a/lib/artists/__tests__/createArtistInDb.test.ts b/lib/artists/__tests__/createArtistInDb.test.ts
index e979fbb..9a02b15 100644
--- a/lib/artists/__tests__/createArtistInDb.test.ts
+++ b/lib/artists/__tests__/createArtistInDb.test.ts
@@ -133,4 +133,17 @@ describe("createArtistInDb", () => {
 
     expect(result).toBeNull();
   });
+
+  it("returns null when linking artist to organization fails", async () => {
+    mockInsertAccount.mockResolvedValue(mockAccount);
+    mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
+    mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
+    mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
+    mockAddArtistToOrganization.mockResolvedValue(null);
+
+    const result = await createArtistInDb("Test Artist", "owner-456", "org-789");
+
+    expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789");
+    expect(result).toBeNull();
+  });
 });
diff --git a/lib/artists/createArtistInDb.ts b/lib/artists/createArtistInDb.ts
index e1eeceb..3990e08 100644
--- a/lib/artists/createArtistInDb.ts
+++ b/lib/artists/createArtistInDb.ts
@@ -44,7 +44,8 @@ export async function createArtistInDb(
 
     // Step 5: Link to organization if provided
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const organizationLinkId = await addArtistToOrganization(account.id, organizationId);
+      if (!organizationLinkId) return null;
     }
 
     return {
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..8a81851 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -41,7 +41,8 @@ export async function createWorkspaceInDb(
     if (!linkId) return null;
 
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const organizationLinkId = await addArtistToOrganization(account.id, organizationId);
+      if (!organizationLinkId) return null;
     }
 
     return {

Analysis

Organization linking failures silently ignored in workspace and artist creation

What fails: createWorkspaceInDb() and createArtistInDb() return the created workspace/artist object and allow the API to return 201 (success) even when addArtistToOrganization() fails to link the workspace/artist to the requested organization.

How to reproduce:

  1. Create a workspace with an organizationId parameter: POST /api/workspaces with body {"name":"Test","organization_id":"org-123"}
  2. The Supabase upsert in addArtistToOrganization() fails (database constraint violation, timeout, permission error, etc.)
  3. The function returns null to indicate failure
  4. The calling code ignores this return value and continues execution
  5. The handler receives the workspace object and returns 201 Created with the workspace data

Result: API returns HTTP 201 with the workspace object, indicating success. However, the workspace is NOT linked to the requested organization.

Expected: When organization linking is explicitly requested and fails, the entire workspace/artist creation should fail. The API should return a 500 error and the workspace should not be created (or should be rolled back). This is consistent with how other critical operations are handled:

  • If insertAccountInfo() returns null → function returns null → API returns 500
  • If insertAccountWorkspaceId() returns null → function returns null → API returns 500
  • If addArtistToOrganization() returns null → function should return null → API should return 500

The fix validates the return value from addArtistToOrganization() and propagates the failure, consistent with the error-handling pattern established in the handler code (addArtistToOrgHandler.ts checks the return value and returns 500 if it fails).

Files fixed:

  • lib/workspaces/createWorkspaceInDb.ts (lines 43-45)
  • lib/artists/createArtistInDb.ts (lines 43-47)
  • lib/artists/__tests__/createArtistInDb.test.ts (added test case for organization linking failure)

Comment on lines +31 to +42
try {
const account = await insertAccount({ name });

const accountInfo = await insertAccountInfo({ account_id: account.id });
if (!accountInfo) return null;

const workspace = await selectAccountWithSocials(account.id);
if (!workspace) return null;

const linkId = await insertAccountWorkspaceId(accountId, account.id);
if (!linkId) return null;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If insertAccountInfo fails, an orphaned account record is left in the database while the API returns a 500 error to the user.

View Details
📝 Patch Details
diff --git a/lib/supabase/accounts/deleteAccount.ts b/lib/supabase/accounts/deleteAccount.ts
new file mode 100644
index 0000000..28379eb
--- /dev/null
+++ b/lib/supabase/accounts/deleteAccount.ts
@@ -0,0 +1,18 @@
+import supabase from "../serverClient";
+
+/**
+ * Deletes an account by its ID
+ *
+ * @param accountId - The ID of the account to delete
+ * @returns true if the delete was successful, false otherwise
+ */
+export async function deleteAccount(accountId: string): Promise<boolean> {
+  const { error } = await supabase.from("accounts").delete().eq("id", accountId);
+
+  if (error) {
+    console.error("[ERROR] deleteAccount:", error);
+    return false;
+  }
+
+  return true;
+}
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..21b98e9 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -1,4 +1,5 @@
 import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
+import { deleteAccount } from "@/lib/supabase/accounts/deleteAccount";
 import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
 import {
   selectAccountWithSocials,
@@ -32,13 +33,25 @@ export async function createWorkspaceInDb(
     const account = await insertAccount({ name });
 
     const accountInfo = await insertAccountInfo({ account_id: account.id });
-    if (!accountInfo) return null;
+    if (!accountInfo) {
+      // Clean up the orphaned account record if account_info creation fails
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const workspace = await selectAccountWithSocials(account.id);
-    if (!workspace) return null;
+    if (!workspace) {
+      // Clean up if we can't retrieve the workspace
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const linkId = await insertAccountWorkspaceId(accountId, account.id);
-    if (!linkId) return null;
+    if (!linkId) {
+      // Clean up if workspace link creation fails
+      await deleteAccount(account.id);
+      return null;
+    }
 
     if (organizationId) {
       await addArtistToOrganization(account.id, organizationId);

Analysis

Orphaned account records left in database when workspace creation partially fails

What fails: createWorkspaceInDb() can leave an orphaned account record in the database if any operation after insertAccount() fails, while the API returns a 500 error to the user. The account is created and committed to the database, but subsequent operations (particularly insertAccountInfo()) may fail due to database errors, network issues, or permission problems. When these operations fail, the account record remains in the database despite the overall workspace creation being reported as failed.

How to reproduce:

// Call createWorkspaceInDb with conditions that cause insertAccountInfo to fail
// Example: Database temporarily unavailable after account insertion
// This will insert the account but then return null when insertAccountInfo fails
const workspace = await createWorkspaceInDb("Test Workspace", "owner-account-id");
// Result: Returns null (500 error to user)
// Side effect: Account record exists in database with no corresponding account_info record

Result: Orphaned account record exists in the database. The account was successfully inserted but account_info and subsequent operations failed, leaving partial state in the database.

Expected: Either all operations succeed and a complete workspace is created, or none persist any state. The operation should be atomic from the user's perspective.

Fix implemented: Added cleanup logic to delete the orphaned account record when any subsequent operation fails. Created deleteAccount() function and updated createWorkspaceInDb() to call it on failure paths, ensuring no orphaned records are left in the database.

Technical background: Supabase does not support client-side transactions directly (unlike traditional ORMs). Operations are executed sequentially, so partial failures can occur. The fix ensures data consistency by cleaning up partial state before returning null to the caller.

- Create validateAuthContext utility as single source of truth for auth
- Fix personal API keys unable to add workspaces to orgs they're members of
- Add self-access check allowing personal keys to specify own account_id
- Refactor validateCreateWorkspaceBody to use centralized utility
- Refactor validateCreateArtistBody to use centralized utility + add missing org validation
- Add comprehensive tests for validateAuthContext (15 tests)
Copy link

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestions:

  1. Organization linking failure via addArtistToOrganization is not validated - the function ignores the return value and continues, causing API to return success even when workspace/artist fails to link to organization
View Details
📝 Patch Details
diff --git a/lib/artists/createArtistInDb.ts b/lib/artists/createArtistInDb.ts
index e1eeceb..dd9209d 100644
--- a/lib/artists/createArtistInDb.ts
+++ b/lib/artists/createArtistInDb.ts
@@ -44,7 +44,8 @@ export async function createArtistInDb(
 
     // Step 5: Link to organization if provided
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const orgLink = await addArtistToOrganization(account.id, organizationId);
+      if (!orgLink) return null;
     }
 
     return {
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..a763194 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -41,7 +41,8 @@ export async function createWorkspaceInDb(
     if (!linkId) return null;
 
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const orgLink = await addArtistToOrganization(account.id, organizationId);
+      if (!orgLink) return null;
     }
 
     return {

Analysis

Bug Explanation

The issue manifests in two files:

  1. lib/workspaces/createWorkspaceInDb.ts (line 45)
  2. lib/artists/createArtistInDb.ts (line 48)

Both functions call addArtistToOrganization() without checking its return value. The function has this signature:

addArtistToOrganization(artistId: string, organizationId: string): Promise<string | null>

It returns null on failure (when the Supabase upsert fails). However, when this function is called with an organizationId parameter, the return value is completely ignored. If organization linking fails, the workspace/artist creation still completes successfully and returns a 201 response to the API, even though the requested organization link was never established.

This violates the fail-fast pattern already established in the same codebase. For comparison, insertAccountWorkspaceId() returns string | null and IS properly validated on line 40 of createWorkspaceInDb.ts with if (!linkId) return null;. The organization linking should follow the same pattern.

Fix Explanation

I've added validation of the addArtistToOrganization() return value in both functions:

  1. createWorkspaceInDb.ts: Capture the return value and check if it's null. If organization linking fails, the entire workspace creation returns null (failure), preventing a false success response.

  2. createArtistInDb.ts: Apply the same fix for consistency and correctness.

The changes follow the existing error-handling pattern in the codebase where all database operations that can fail are checked before proceeding. This ensures the API returns appropriate error responses when organization linking fails, rather than falsely indicating success.

  1. Orphaned account records left in database when subsequent operations fail in createWorkspaceInDb function
View Details
📝 Patch Details
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..e9936ec 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -1,4 +1,5 @@
 import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
+import { deleteAccount } from "@/lib/supabase/accounts/deleteAccount";
 import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
 import {
   selectAccountWithSocials,
@@ -32,13 +33,22 @@ export async function createWorkspaceInDb(
     const account = await insertAccount({ name });
 
     const accountInfo = await insertAccountInfo({ account_id: account.id });
-    if (!accountInfo) return null;
+    if (!accountInfo) {
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const workspace = await selectAccountWithSocials(account.id);
-    if (!workspace) return null;
+    if (!workspace) {
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const linkId = await insertAccountWorkspaceId(accountId, account.id);
-    if (!linkId) return null;
+    if (!linkId) {
+      await deleteAccount(account.id);
+      return null;
+    }
 
     if (organizationId) {
       await addArtistToOrganization(account.id, organizationId);

Analysis

Detailed Bug Explanation

Why it happens:
The createWorkspaceInDb function performs a multi-step database operation sequence:

  1. Creates an account record via insertAccount() which commits immediately
  2. Creates account_info via insertAccountInfo()
  3. Retrieves workspace data via selectAccountWithSocials()
  4. Links workspace to owner via insertAccountWorkspaceId()

However, if any step after insertAccount() fails and returns null, the function returns null without cleaning up the already-committed account record.

When it manifests:

  • If insertAccountInfo() fails (line 34-35): account exists with no account_info
  • If selectAccountWithSocials() fails (line 37-38): account and account_info exist but workspace selection failed
  • If insertAccountWorkspaceId() fails (line 40-41): account exists but workspace link wasn't created

Impact:
Database accumulates orphaned account records without corresponding related data, causing:

  • Data integrity issues
  • Orphaned records consuming storage
  • Potential foreign key constraint violations if the orphaned accounts are referenced elsewhere
  • Operational confusion when auditing accounts in the database

Fix Explanation

Changes made:

  1. Created new lib/supabase/accounts/deleteAccount.ts utility function to safely delete accounts by ID
  2. Updated createWorkspaceInDb to import and call deleteAccount(account.id) before returning null at each failure point:
    • After insertAccountInfo failure
    • After selectAccountWithSocials failure
    • After insertAccountWorkspaceId failure

Why this solves it:
By calling deleteAccount() before returning null at each failure point, the function now maintains database consistency. If any operation fails after the account is created, the entire account record (and its cascading relationships if configured) is properly cleaned up, preventing orphaned records from accumulating in the database.

The fix follows the principle of transactional consistency - either the entire workspace creation succeeds, or all intermediate records are cleaned up on failure.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

This is the final PR Bugbot will review for you during this billing cycle

Your free Bugbot reviews will reset on February 17

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

const keyDetails = await getApiKeyDetails(apiKey!);
if (keyDetails) {
orgId = keyDetails.orgId;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check causes silent org context loss

Medium Severity

When getApiKeyDetails returns null (e.g., due to a transient error in getAccountOrganizations), the code silently continues with orgId = null instead of returning an error. The original validateCreateArtistBody code returned a 401 "Invalid API key" error in this case. Now, org API keys experiencing this edge case are treated as personal keys, causing users to get 403 "Access denied to specified account_id" errors when attempting account overrides, rather than a clear authentication error that would prompt a retry.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants